/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.ListBox
{
  internal class ScrollManager : DependencyObject
  {
    private const int KillSwitchThreshold = 25;
    private const int RoundingThreshold = 6;

    #region Constructors

    internal ScrollManager( PanelBase parentPanel )
    {
      m_parentPanel = parentPanel;
    }

    #endregion

    #region IsScrollManagerActive Internal Property

    internal bool IsScrollManagerActive
    {
      get;
      private set;
    }

    #endregion

    #region TargetIndex Internal Property

    internal double TargetIndex
    {
      get;
      private set;
    }

    #endregion

    #region ActualScrollIndex Internal Property

    internal double ActualScrollIndex
    {
      get;
      set;
    }

    #endregion // ActualScrollIndex Property

    #region AnimatedActualScrollIndex Internal Property

    private static readonly DependencyProperty AnimatedActualScrollIndexProperty = DependencyProperty.Register(
        "AnimatedActualScrollIndex", 
        typeof( double ), 
        typeof( ScrollManager ),
        new PropertyMetadata( 0d, new PropertyChangedCallback( ScrollManager.OnAnimatedActualScrollIndexChanged ) ) );

    private double AnimatedActualScrollIndex
    {
      get
      {
        return ( double )this.GetValue( ScrollManager.AnimatedActualScrollIndexProperty );
      }
      set
      {
        this.SetValue( ScrollManager.AnimatedActualScrollIndexProperty, value );
      }
    }

    private static void OnAnimatedActualScrollIndexChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      var scrollManager = sender as ScrollManager;

      scrollManager.ActualScrollIndex = ( double )e.NewValue;
      scrollManager.RaiseActualScrollIndexChanged();
    }

    #endregion // ActualScrollIndex Property

    #region AvertageItemSize Internal Property

    internal double AverageItemSize
    {
      get;
      set;
    }

    #endregion

    #region ActiveNavigationAction

    internal PanelNavigationActionBase ActiveNavigationAction
    {
      get;
      private set;
    }

    #endregion

    #region Internal Events

    internal event EventHandler ActualScrollIndexChanged;

    internal event EventHandler<ScrollCompletedEventArgs> ScrollCompleted;

    #endregion

    #region Internal Methods

    internal void ForceScrollIndex( double originalIndex, double forcedIndex )
    {
      double difference = forcedIndex - originalIndex;

      this.ActualScrollIndex = Math.Max( 0, this.ActualScrollIndex + difference );
      this.TargetIndex = Math.Max( 0, this.TargetIndex + difference );
      m_startIndex = Math.Max( 0, m_startIndex + difference );
    }

    internal double RoundIndexToClosestItem( double index )
    {
      //Rounding: If the position requested would display less than "x" Pixels of the item, round it down to next item.
      if( this.AverageItemSize > 0d )
      {
        double computedRoundingThreshold = ScrollManager.RoundingThreshold / this.AverageItemSize;
        double computedBottomRoundingThreshold = 1d - computedRoundingThreshold;

        double modulo = index % 1;

        if( modulo != 0d )
        {
          if( modulo > computedBottomRoundingThreshold )
          {
            index = Math.Ceiling( index );
          }
          else if( modulo < computedRoundingThreshold )
          {
            index = Math.Floor( index );
          }
        }
      }

      return index;
    }

    internal void ScrollToIndex( double index, int duration, IEasingFunction easingFunction, PanelNavigationActionBase navigationAction )
    {
      ListBox parentListBox = m_parentPanel.ParentListBox;

      // if ListBox is locked, do not scroll
      if( ( parentListBox != null ) && ( parentListBox.IsLocked ) )
        return;

      bool alreadyRunning = this.IsScrollManagerActive;

      if( alreadyRunning )
      {
        m_scrollStoryboard.Pause();
      }

      if( this.ActiveNavigationAction != navigationAction )
      {
        this.ActiveNavigationAction = navigationAction;
      }

      this.TargetIndex = index;
      m_startIndex = this.ActualScrollIndex;
      m_animationDuration = duration;
      m_animationEasingFunction = easingFunction;

      if( duration > 0 )
      {
        this.StartScrollStoryboard();

        this.IsScrollManagerActive = true;
      }
      else
      {
        this.StopScrollStoryboard();

        this.ActualScrollIndex = this.TargetIndex;

        this.RaiseActualScrollIndexChanged();
        this.RaiseScrollingCompleted();
      }
    }

    internal void UpdateAnimationSettings( double scrollableRange )
    {
      if( IsScrollManagerActive )
      {
        // Since scrollableRange may change in function of viewport size, check if target is still valid
        if( this.TargetIndex > scrollableRange )
        {
          this.UpdateScrollAnimationTarget( scrollableRange );
        }
        else if( this.ActiveNavigationAction != null )
        {
          double updatedScrollTarget = this.ActiveNavigationAction.CalculateTargetScrollIndex();

          if( !LayoutUtils.AreClose( updatedScrollTarget, this.TargetIndex ) )
          {
            this.UpdateScrollAnimationTarget( updatedScrollTarget );
          }
        }
      }
    }

    private void UpdateScrollAnimationTarget( double newTarget )
    {
      var animation = m_scrollStoryboard.Children[ 0 ] as DoubleAnimation;

      if( ( animation != null ) && ( animation.From.HasValue ) && ( animation.To.HasValue ) )
      {
        double previousToValue = animation.To.Value;
        double previousFromValue = animation.From.Value;
        double normalizedElapsedTime = ( this.AnimatedActualScrollIndex - previousFromValue ) / ( previousToValue - previousFromValue );

        if( normalizedElapsedTime < 1d )
        {
          animation.To = newTarget;
          animation.From -= ( normalizedElapsedTime * ( newTarget - previousToValue ) ) / ( 1 - normalizedElapsedTime );
        }
      }

      this.TargetIndex = newTarget;
    }

    #endregion

    #region Private Methods

    private void Shutdown()
    {
      this.StopScrollStoryboard();

      if( !this.IsScrollManagerActive )
        return;

      //m_panelBase.ClearValue( TextOptions.TextHintingModeProperty );

      this.RaiseScrollingCompleted();
    }

    private void RaiseActualScrollIndexChanged()
    {
      if( this.ActualScrollIndexChanged != null )
      {
        this.ActualScrollIndexChanged( this, EventArgs.Empty );
      }
    }

    private void RaiseScrollingCompleted()
    {
      this.IsScrollManagerActive = false;

      double averageContainerSize = this.m_parentPanel.GetAverageContainerSize();

      // When animation is completed, the actual scroll index should be at the desired target.
      if( ( this.ActiveNavigationAction != null ) &&
        ( !LayoutUtils.AreClose( this.ActualScrollIndex * averageContainerSize, this.ActiveNavigationAction.CalculateTargetScrollIndex() * averageContainerSize ) ) )
      {
        this.AnimatedActualScrollIndex = this.ActiveNavigationAction.CalculateTargetScrollIndex();
      }

      if( this.ScrollCompleted != null )
      {
        this.ScrollCompleted( this, new ScrollCompletedEventArgs( this.ActiveNavigationAction ) );
      }

      this.ActiveNavigationAction = null;
    }

    private void StartScrollStoryboard()
    {
      this.StopScrollStoryboard();

      DoubleAnimation animation = new DoubleAnimation();
      animation.From = m_startIndex;
      animation.To = this.TargetIndex;
      animation.Duration = TimeSpan.FromMilliseconds( m_animationDuration );
      animation.EasingFunction = m_animationEasingFunction;

      Storyboard.SetTarget( animation, this );
      Storyboard.SetTargetProperty( animation, new PropertyPath( ScrollManager.AnimatedActualScrollIndexProperty ) );

      m_scrollStoryboard = new Storyboard();
      m_scrollStoryboard.Children.Add( animation );
      m_scrollStoryboard.Completed += new EventHandler( this.OnScrollStoryboardCompleted );
      m_scrollStoryboard.Begin();
    }

    private void StopScrollStoryboard()
    {
      if( m_scrollStoryboard == null )
        return;

      m_scrollStoryboard.Completed -= new EventHandler( this.OnScrollStoryboardCompleted );
      m_scrollStoryboard.Pause();
      m_scrollStoryboard = null;
    }

    private void OnScrollStoryboardCompleted( object sender, EventArgs e )
    {
      this.Shutdown();
    }

    #endregion

    #region Private Fields

    private Storyboard m_scrollStoryboard;

    private double m_startIndex;
    private int m_animationDuration;
    private IEasingFunction m_animationEasingFunction;

    private PanelBase m_parentPanel;

    #endregion
  }
}
